/****************************************************************************
 *
 *  LinuxChatty
 *  Copyright (C) 2010, David Hsu.
 * 
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 ***************************************************************************/
#include "ScrollableView.h"
#include "services/StringUtil.h"
#include <ncurses.h>
#include <cctype>

using namespace std;

typedef map< unsigned int, stack<unsigned int> >::iterator MapIterator;

/**
 * Constructor
 * @param info Window information with width, height, top, and left
 */
ScrollableView::ScrollableView(WindowInfo info):
	Window(info),
	topRow(0),
	spoilerPosition(-1),
	displayStartIndex(0),
	displayEndIndex(0)
{
	// Nothing else
}

/**
 * Set the text to display in the scrollable view. Note that this also does
 * word wrapping.
 * @param text String to display in the view
 */
void ScrollableView::SetText(const string& text)
{	
	ClearText();
	ParseComment(text);
	StringUtil::WordWrapText(currentComment, textArray, windowInfo.width);
	FormatTextArray();
    
    // Initialize the display indices
    CalculateDisplayIndex();
}

/**
 * Removes all text from the view.
 */
void ScrollableView::ClearText()
{
	textArray.clear();
	spoilerMap.clear();
	topRow = 0;
	spoilerPosition = 0;
    displayStartIndex = 0;
	displayEndIndex = 0;
}

/**
 * Scrolls the view.
 * @param distance An integer distance to scroll, can be negative
 */
void ScrollableView::Scroll(const int distance)
{   
    // See if we're scrolling through spoiler tags. If there's only one spoiler
    // tag, then there's no need to scroll through them.
    if (spoilerMap.size() > 1)
    {
        int currentSelected = spoilerPosition;
        MapIterator mit = spoilerMap.find(spoilerPosition);
        
        // If advancing down, get the next spoiler position that isn't inside
        // the current spoiler. Note that in both cases, it'll check to see if
        // it's within the current window view.
        if (distance > 0)
        {
            // Get the ending index for the current index
            unsigned int endIndex = spoilerMap[spoilerPosition].top();
            
            // Start the iterator on the next entry
            for (mit++; mit != spoilerMap.end(); mit++)
            {
                // If the next spoiler isn't embedded inside the original
                // spoiler, then this is the next one assuming it's still
                // in the current window view.
                if (mit->second.top() > endIndex)
                {
                    // Check to see if it's within the view. If it isn't, then
                    // we won't change the position.
                    if (mit->first < displayEndIndex)
                    {
                        currentSelected = mit->first;
                    }
                    break;
                }
            }
        }
        else
        {
            currentSelected = FindPreviousSpoiler();
        }
        
        // If the position changed, then this is all that's required for
        // the scroll.
        if (spoilerPosition != currentSelected)
        {
            spoilerPosition = currentSelected;
            return;
        }
    }
    
	// If the text array isn't even fill the screen, then there's no scrolling
	if (textArray.size() < windowInfo.height)
		return;
	
	if ((int)topRow + distance  < 0)
		topRow = 0;
	else
		topRow += distance;
	
	// Bound checking
	if (topRow > textArray.size() - windowInfo.height)
		topRow = textArray.size() - windowInfo.height;

    // Update the index for the display    
    CalculateDisplayIndex();
}

/**
 * Reveals the currently selected spoiler tag, if any is in the comment.
 */
void ScrollableView::RevealCurrentTag()
{
    // Can't clear a tag if it's not currently on the screen or the spoiler map
    // is empty.
    if (spoilerMap.empty() || 
        !(spoilerPosition >= (int)displayStartIndex &&
          spoilerPosition < (int)displayEndIndex) )
    {
        return;
    }
        
    // Using the current spoiler position, pop the top spoiler stack off
    // and restore that section of the text using the raw comment.
    string::iterator it = currentComment.begin();
    unsigned int endIndex = spoilerMap[spoilerPosition].top();
    currentComment.replace( it+spoilerPosition, it+endIndex,
        rawComment.substr(spoilerPosition, endIndex - spoilerPosition) );
    
    // Keep track of the previous spoiler position for deletion
    int previous = spoilerPosition;
    
    // Find the map position for the current spoiler
    MapIterator mit = spoilerMap.find(spoilerPosition);
    
    // Get the next spoiler index if one exists.
    if (mit != spoilerMap.end())
    {
        if (++mit != spoilerMap.end())
        {
            spoilerPosition = mit->first;
        }
        mit--;
    }
    
    // If there's no next spoiler, see if there's a previous        
    if (previous == spoilerPosition)
    {
        spoilerPosition = FindPreviousSpoiler();
    }
    
    // Remove the map key if the stack is empty
    spoilerMap[previous].pop();
    if (spoilerMap[previous].empty())
    {
        spoilerMap.erase(previous);
    }
    
    // After revealing the tag, redraw the screen
    textArray.clear();
    SpoilerComment();
    StringUtil::WordWrapText(currentComment, textArray, windowInfo.width);
    FormatTextArray();
}
	
/**
 * Refreshes the display and redraws the text on the screen.
 */
void ScrollableView::Refresh()
{
	// Draws the text on the screen
	string padding(windowInfo.width, ' ');
	for (unsigned int i = 0; i < windowInfo.height; i++)
	{
		// Clears the line in this section
		mvwprintw(window, i, 0, "%s", padding.c_str());
		
		if (i + topRow < textArray.size())
		{	
			// Prints the text line on the screen
			mvwprintw( window, i, 0, "%s", textArray[i+topRow].c_str() );
		}
	}
	
	// Draw a solid block over the currently selected spoiler block
	if (!spoilerMap.empty())
	{   
	    // With the position of the current spoiler selected, subtract the
	    // current position to get the string position in the array
	    if (spoilerPosition >= (int)displayStartIndex &&
	        spoilerPosition < (int)displayEndIndex)
	    {
	        // Loop through the text and start drawing inverted blocks until
	        // the spoiler is filled.
            unsigned int col = spoilerPosition - displayStartIndex;
            unsigned int currentRowIndex = displayStartIndex;
	        for (unsigned int i = 0; i < windowInfo.height; i++)
	        {
	            // Check to see if the spoiler is in this line. If not, remove
	            // the amount from the counter until we reach a point where the
	            // counter is within a line, which is where the spoiler will be.
	            if (col < textArray[topRow + i].size())
	            {
    	            wattron(window, A_REVERSE);
	                
	                // Begin spoilering text until we either reach the end
	                // of the spoiler tag or the end of the window.
	                while (i < windowInfo.height &&
	                       currentRowIndex + col < spoilerMap[spoilerPosition].top())
	                {
	                    mvwprintw(window, i, col, "%s", " ");
	                    col++;
	                    
	                    if (col > textArray[topRow + i].size())
	                    {
	                        col = 0;
	                        currentRowIndex += textArray[topRow + i].size();
	                        i++;
	                    }
	                }
	                
       	            wattroff(window, A_REVERSE);
       	            break;
	            }
	            else
	            {
	                col -= textArray[topRow + i].size();
	                currentRowIndex += textArray[topRow + i].size();
	            }
	        }
	    }
	}
	
	// mvwprintw(window, 5, 0, "%d", textArray.size());
	// mvwprintw(window, 6, 0, "%d", windowInfo.height);
	// mvwprintw(window, 7, 0, "%d", topRow);
	wrefresh(window);
}

/**
 * Parses and formats the comment text so that spoiler tags work as intended,
 * creates the mapping for selecting, and stores the displayed text.
 * @param comment String comment to be displayed.
 */
void ScrollableView::ParseComment(const string& comment)
{
    // Store the raw comment, which we'll be making modifications in-place
    rawComment = comment;
    
    // Spoiler text is separated by ASCII 2 (beginning tag) and 3 (ending
    // tag), so loop through the text and start extracting the positions for
    // the mapping.
    string::iterator it = rawComment.begin();
    unsigned int strPos = 0;
    
    // A stack will keep track of the beginning indices so it can match with
    // the ending indices it finds.
    stack<unsigned int> beginStack;
    
    // To do this, iterate through the string and look for the tags
    while (it < rawComment.end())
    {
        // If the iterator is over ASCII 2, then mark this position in the map
        // and remove the tag.
        if (*it == 2)
        {   
            beginStack.push(strPos);
            rawComment.erase(it);
        }
        else if (*it == 3)
        {
            // Found an ending tag, so we pop off a tag from the begin stack
            // and add this to the spoiler mapping.
            unsigned int beginIndex = beginStack.top();
            spoilerMap[beginIndex].push(strPos);
            rawComment.erase(it);
            beginStack.pop();
        }
        else
        {
            it++;
            strPos++;
        }
    }
    
    currentComment = rawComment;
    
    // After parsing it, if there's any spoilers, then set the current
    // selected index as the first item and spoiler the text.
    if (!spoilerMap.empty())
    {
        spoilerPosition = spoilerMap.begin()->first;
        SpoilerComment();
    }
}

/**
 * Formats the comment and replaces any active spoilers with udnerscores.
 */
void ScrollableView::SpoilerComment()
{
    // If the spoiler map is empty, then there's nothing to do.
    if (!spoilerMap.empty())
    {
        // Iterate through the spoiler map and start replacing words with
        // underscores.
        MapIterator it;
        for (it = spoilerMap.begin(); it != spoilerMap.end(); it++)
        {
            // it will be the beginning index and the top of the stack for
            // that key will be the ending index.
            // Loop through the subsection of the comment and start replacing
            // words with _. Note that it's keeping the spaces in order for
            // word wrap to work correctly, or else it'll wrap around long
            // blocks when it shouldn't.
            for ( unsigned int i = it->first; i < it->second.top(); i++ )
            {
                if (!isspace(currentComment[i]) || i == it->first)
                {
                    currentComment[i] = '_';
                }
            }
        }
    }
}

/**
 * Helper function that will connect spoiler underscores together so it'll
 * be one big block instead of individual words underlined.
 */
void ScrollableView::FormatTextArray()
{
    // Connect the spoiler _ together so it's one block instead of identifiable
    // underscored words. If there's no spoilers, then there's no need to do
    // this.
    if (!spoilerMap.empty())
    {
        bool spoil = false;
        for (unsigned int i = 0; i < textArray.size(); i++)
        {
            for (unsigned int j = 0; j < textArray[i].size(); j++)
            {
                if (textArray[i][j] == '_')
                {
                    // TODO: Check to see if this is part of the spoiler
                    // index.
                    spoil = true;
                }
                else if (textArray[i][j] == ' ' && spoil)
                {
                    // If the next character is also _, then connect them.
                    if (j + 1 < textArray[i].size() &&
                        textArray[i][j + 1] == '_')
                    {
                        textArray[i][j] = '_';
                    }
                }
                else
                {
                    spoil = false;
                }
            }
        }
    }
}

/**
 * Helper function to calculate the starting and ending index of the string
 * currently displayed.
 */
void ScrollableView::CalculateDisplayIndex()
{
	// Get the current position of the first sentence.
	displayStartIndex = 0;
	displayEndIndex = 0;
    unsigned int strIndex;
    for (strIndex = 0; strIndex < topRow; strIndex++)
    {
        displayStartIndex += textArray[strIndex].size();
    }
    
    displayEndIndex = displayStartIndex;
    
    // Calculate the ending index of the display
    for (unsigned int i = 0; i < windowInfo.height &&
         strIndex + i < textArray.size(); i++)
    {
        displayEndIndex += textArray[strIndex + i].size();
    }
}

/**
 * Finds the previous spoiler tag.
 * @return Integer key of the next spoiler. If one isn't found, returns the
 *         original position.
 */
int ScrollableView::FindPreviousSpoiler()
{
    int currentSelected = spoilerPosition;
    MapIterator mit = spoilerMap.find(spoilerPosition);
    
    // Check a previous spoiler by first checking to see if the current
    // iterator is over the beginning. If it is, there's no more
    // spoilers. Otherwise, when going up, find a previous spoiler that
    // encompasses all the previous.
    while ( mit-- != spoilerMap.begin() )
    {
        // If it's still the current spoiler position, then use the
        // previous index as the current selected before doing the
        // checks.
        if (currentSelected == spoilerPosition &&
            mit->first >= displayStartIndex)
        {
            currentSelected = mit->first;
        }
        else
        {
            // Check to see if the current end index is within the
            // previous item. If it is, then the previous item
            // is the new selected as it encompasses the current.
            if (spoilerMap[currentSelected].top() <
                mit->second.top())
            {
                currentSelected = mit->first;
            }
            else
            {
                // If it's not contained by the previous, then this
                // is the new spoiler index.
                break;
            }
        }
    }
    
    return currentSelected;
}
